/**
 * git客户端操作
 *
 * Created by snowingsea on 2020/02/07.
 */
const fs = require('fs-extra');
const path = require('path');
const SimpleGit = require('simple-git/promise');
const iconv = require('iconv-lite');
const { spawn } = require('child_process');
const gitServer = require('./git-sever');
const utility = require('./../utility');
const cacheUtil = require('./../../cacheUtil');
const projectName = require('./../../../package').name;

const diffType = {
  add: 'A', // 添加
  copy: 'C', // 复制
  delete: 'D', // 删除
  modify: 'M', // 修改
  rename: 'R', // 重命名
};

class GitClient {
  constructor(workingDirPath) {
    this.client = SimpleGit(workingDirPath);
    this.initBranchName = 'master';
  }

  /**
   * 仓库初始化
   * @return {Promise}
   */
  init() {
    return new Promise((resolve, reject) => {
      this.client.init()
        .then(() => this.initConfig())
        .then(resolve)
        .catch(reject);
    });
  }

  initConfig() {
    return new Promise((resolve, reject) => {
      this.client.addConfig('user.name', projectName)
        .then(() => this.client.addConfig('user.email', `${projectName}@local.com`))
        .then(() => {
          const { gitDir, gitIgnoreGlobalName } = global.config.path;
          const filePath = path.join(path.resolve(gitDir), gitIgnoreGlobalName);
          return this.client.addConfig('core.excludesfile', filePath);
        })
        .then(() => this.client.addConfig('core.quotepath', 'off'))
        .then(() => this.client.addConfig('core.longpaths', 'true'))
        .then(resolve)
        .catch(reject);
    });
  }

  /**
   * 添加所有文件变化
   * @param {String} commitMessage 提交信息
   * @param {String} branchName 分支名
   * @return {Promise}
   */
  addAll(commitMessage = 'add all changes') {
    return new Promise((resolve, reject) => {
      this.client.add(['--all', '.'])
        .then(() => this.client.commit(commitMessage))
        .then(resolve)
        .catch(reject);
    });
  }

  /**
   * 创建分支
   * @param {String} branchName 分支名
   * @return {Promise}
   */
  createBranch(branchName, fromBranch = null) {
    return new Promise((resolve, reject) => {
      this.removeBranch(branchName)
        .then(() => this.client.checkout(['-b', branchName, fromBranch || this.initBranchName]))
        .then(resolve)
        .catch(reject);
    });
  }

  /**
   * 检出分支
   * @param {String} branchName 分支名
   * @return {Promise}
   */
  checkoutBranch(branchName) {
    return new Promise((resolve, reject) => {
      this.client.checkout(['-f', branchName])
        .then(resolve)
        .catch(reject);
    });
  }

  checkoutCommit(commitId) {
    return new Promise((resolve, reject) => {
      this.client.checkout(['-f', commitId])
        .then(resolve)
        .catch(reject);
    });
  }

  show(commitId, filePath) {
    return new Promise((resolve, reject) => {
      this.client.show([`${commitId}:${filePath}`])
        .then((fileData) => {
          resolve(fileData);
        })
        .catch(reject);
    });
  }

  showStream(commitId, filePath, encoding) {
    return new Promise((resolve) => {
      this.client.outputHandler((command, stdout) => {
        if (encoding) {
          const converterStream = iconv.decodeStream(encoding);
          resolve(converterStream);
          stdout.pipe(converterStream);
        } else {
          resolve(stdout);
        }
      })
        .show([`${commitId}:${filePath}`]);
    });
  }

  /**
   * 删除分支
   * @param {String} branchName 分支名
   * @return {Promise}
   */
  removeBranch(branchName) {
    if (branchName === this.initBranchName) {
      return Promise.resolve();
    }
    return new Promise((resolve, reject) => {
      this.client.checkout(this.initBranchName)
        .then(() => this.client.branch())
        .then(({ all }) => {
          const branches = new Set(all);
          if (branches.has(branchName)) {
            return this.client.branch(['-D', branchName]);
          }
          return Promise.resolve();
        })
        .then(resolve)
        .catch(reject);
    });
  }

  /**
   * 获取最新提交id
   * @return {Promise}
   */
  getLastedCommitId() {
    return new Promise((resolve, reject) => {
      this.client.log(['-1'])
        .then(({ latest }) => {
          resolve(latest.hash);
        })
        .catch(reject);
    });
  }

  /**
   * 获取diff信息
   * @param {String} commitId1
   * @param {String} commitId2
   * @return {Promise}
   */
  diffSummary(commitId1, commitId2) {
    return this.client.diffSummary(['--no-renames', commitId1, commitId2]);
  }

  /**
   * 获取diff信息,并格式化
   * @param {*} commitId1
   * @param {*} commitId2
   * @returns
   */
  diffSummaryFormat(commitId1, commitId2) {
    const promises = [];
    promises.push(this.client.diff(['--no-renames', '--name-status', '--stat', commitId1, commitId2]));
    promises.push(this.client.diff(['--no-renames', '--numstat', commitId1, commitId2]));
    return Promise.all(promises);
  }

  /**
   * 克隆仓库
   * @param {String} remoteURL git远程地址
   * @param {String} localPath 本地仓库路径
   * @param {Array} options 参数
   * @return {Promise}
   */
  static clone(remoteURL, localPath = undefined, options = []) {
    return SimpleGit().silent(true).clone(remoteURL, localPath, options);
  }
}
function isNewEnviroment(version) {
  return version
        && version.repository
        && version.repository.url
        && version.repository.url === gitServer.getRemoteURL({ _id: version.projectId }, version);
}
/**
 * 获取本地用户路径
 * @param {User} user 用户信息
 * @return {String}
 */
function getLocalUserPath(user) {
  return path.join(global.config.path.gitDir, gitServer.getGitUserName(user));
}

/**
 * 获取本地仓库路径
 * @param {User} user 用户信息
 * @param {Project} project 项目信息
 * @param {Version} version 版本信息
 * @return {String}
 */
function getLocalRepoPath(user, project, version) {
  if (isNewEnviroment(version)) {
    return path.join(getLocalUserPath(project), gitServer.getGitRepoName(version));
  }
  return path.join(getLocalUserPath(user), gitServer.getGitRepoName(project));
}

/**
 * 获取本地分支仓库路径
 * @param {User} user 用户信息
 * @param {Project} project 项目信息
 * @param {ProjectVersion} version 项目版本信息
 * @return {string}
 */
function getLocalBranchPath(user, project, version) {
  return path.join(getLocalRepoPath(user, project), gitServer.getBranchName(version));
}

/**
 * 初始化git库
 * @param {User} user 用户信息
 * @param {Project} project 项目信息
 * @return {Promise}
 */
function initRepo(user, project) {
  return new Promise((resolve, reject) => {
    const localRepoPath = getLocalRepoPath(user, project);
    let gitClient;

    fs.remove(localRepoPath)
      .then(() => fs.ensureDir(localRepoPath))
      .then(() => utility.writeFile('', path.join(localRepoPath, '.gitignore')))
      .then(() => {
        gitClient = new GitClient(localRepoPath);
        return gitClient.init();
      })
      .then(() => gitClient.addAll('init'))
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 清空本地仓库某个
 * @param {User} user 用户
 * @param {Project} project 项目
 * @param {String} dir 文件夹路径（相对路径）
 * @param {String} exceptFileName 不删除特殊文件
 * @param {Object} version 版本信息
 * @return {Promise}
 */
function emptyRepoDirectory(user, project, dir = '.', exceptFileName = '', version) {
  return new Promise((resolve, reject) => {
    const localRepoPath = getLocalRepoPath(user, project, version);
    const dirPath = path.join(localRepoPath, dir);
    fs.readdir(dirPath)
      .then((resFiles) => {
        const promises = resFiles.map((fileName) => {
          if (fileName === '.git') {
            return Promise.resolve();
          }
          if (fileName === exceptFileName) {
            return Promise.resolve();
          }
          return fs.remove(path.join(dirPath, fileName));
        });
        return Promise.all(promises);
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 移除.git文件夹
 * @param {String} dir 当前文件夹
 * @return {Promise}
 */
function removeGitDirectory(dir) {
  return new Promise((resolve, reject) => {
    let isDirectory = false;
    fs.stat(dir)
      .then((stats) => {
        if (stats.isDirectory()) {
          isDirectory = true;
          return fs.remove(path.join(dir, '.git'));
        }
        return Promise.resolve();
      })
      .then(() => {
        if (isDirectory) {
          return fs.readdir(dir);
        }
        return Promise.resolve([]);
      })
      .then((files) => {
        const promises = files.map((file) => removeGitDirectory(path.join(dir, file)));
        return Promise.all(promises);
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 检出/同步分支
 * @param {User} user 用户信息
 * @param {Project} project 项目信息
 * @param {ProjectVersion} version 项目版本信息
 * @param {String} commitId 版本号
 * @return {Promise}
 */
function checkoutBranch(user, project, version, commitId = null) {
  const localRepoPath = getLocalRepoPath(user, project, version);
  const gitClient = new GitClient(localRepoPath);
  if (commitId) {
    return gitClient.checkoutCommit(commitId);
  }
  return gitClient.checkoutBranch(gitServer.getBranchName(version));
}

/**
 * 创建分支
 * @param {User} user 用户信息
 * @param {Project} project 项目信息
 * @param {ProjectVersion} version 项目版本信息
 * @return {Promise}
 */
function createBranch(user, project, version, branchName = null, fromBranch = null) {
  const localRepoPath = getLocalRepoPath(user, project, version);
  if (!branchName) {
    branchName = gitServer.getBranchName(version);
  }
  const gitClient = new GitClient(localRepoPath);
  return gitClient.createBranch(branchName, fromBranch);
}

/**
 * 删除分支
 * @param {User} user 用户
 * @param {Project} project 项目
 * @param {ProjectVersion} version 项目版本
 * @return {Promise}
 */
function removeBranch(user, project, version, branchName = null) {
  const localRepoPath = getLocalRepoPath(user, project);
  if (!branchName) {
    branchName = gitServer.getBranchName(version);
  }
  const gitClient = new GitClient(localRepoPath);
  return gitClient.removeBranch(branchName);
}

/**
 * 为所有文件夹添加gitignore文件
 * @param {String} dir 文件夹
 * @return {Promise}
 */
function addGitIgnore(dir) {
  return new Promise((resolve, reject) => {
    let isDirectory = false;
    fs.stat(dir)
      .then((stats) => {
        if (stats.isDirectory()) {
          isDirectory = true;
          return utility.writeFile('', path.join(dir, '.gitignore'));
        }
        return Promise.resolve();
      })
      .then(() => {
        if (isDirectory) {
          return fs.readdir(dir);
        }
        return Promise.resolve([]);
      })
      .then((files) => {
        const promises = files.map((file) => addGitIgnore(path.join(dir, file)));
        return Promise.all(promises);
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 添加某个文件夹下的所有文件
 * @param {String} dirPath 文件夹路径
 * @param {User} user 用户
 * @param {Project} project 项目
 * @param {ProjectVersion} version 项目版本
 * @return {Promise}
 */
function addAllFilesInDirectory(dirPath, user, project, version) {
  return new Promise((resolve, reject) => {
    const localRepoPath = getLocalRepoPath(user, project, version);
    if (!path.isAbsolute(dirPath)) {
      dirPath = path.join(localRepoPath, dirPath);
    }
    if (!dirPath.startsWith(localRepoPath)) {
      resolve();
      return;
    }
    fs.pathExists(dirPath)
      .then((exists) => {
        if (exists) {
          return addGitIgnore(dirPath);
        }
        return Promise.resolve();
      })
      .then(() => {
        const gitClient = new GitClient(localRepoPath);
        let relativePath = path.relative(localRepoPath, dirPath);
        if (relativePath === '') {
          relativePath = '.';
        }
        return gitClient.addAll(`add all changes of ${relativePath}`);
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 获取最新提交id
 * @param {User} user 用户
 * @param {Project} project 项目
 * @param {ProjectVersion} version 项目版本
 * @return {Promise}
 */
function getLatestCommitId(user, project, version) {
  return new Promise((resolve, reject) => {
    checkoutBranch(user, project, version)
      .then(() => {
        const localRepoPath = getLocalRepoPath(user, project, version);
        const gitClient = new GitClient(localRepoPath);
        return gitClient.getLastedCommitId();
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 获取diff信息
 * @param {User} user 用户
 * @param {Project} project 项目
 * @param {String} commitId1
 * @param {String} commitId2
 * @return {Promise}
 */
function diffSummary(user, project, version, commitId1, commitId2) {
  const localRepoPath = getLocalRepoPath(user, project, version);
  const gitClient = new GitClient(localRepoPath);
  return gitClient.diffSummary(commitId1, commitId2);
}

async function diffSummaryFormat(user, project, currentCommitId, targetCommitId) {
  const localRepoPath = getLocalRepoPath(user, project);
  const gitClient = new GitClient(localRepoPath);
  const [statusInfo, numberInfo] = await gitClient.diffSummaryFormat(currentCommitId, targetCommitId);
  const fileToNumberMap = new Map(numberInfo.split('\n').filter((str) => str).map((str) => {
    const [insertion, deletion, file] = str.split('\t');
    return [file, [Number(insertion), Number(deletion)]];
  }));
  const files = statusInfo.split('\n').filter((str) => str).map((str) => {
    const [type, file] = str.split('\t');
    if (fileToNumberMap.has(file)) {
      return {
        type,
        file,
        insertion: fileToNumberMap.get(file)[0],
        deletion: fileToNumberMap.get(file)[1],
      };
    }
    return {
      type, file, insertion: 0, deletion: 0,
    };
  });
  return { files };
}

async function show(userId, projectId, version, commitId, filePath, useStream = false, encoding) {
  const user = { _id: userId };
  const project = { _id: projectId };
  const gitClient = new GitClient(getLocalRepoPath(user, project, version));
  if (useStream) {
    return gitClient.showStream(commitId.toString(), filePath, encoding);
  }
  return gitClient.show(commitId.toString(), filePath);
}

async function archive(user, project, version) {
  const exportPath = path.join(cacheUtil.getConfigPath('exportDir'), project._id.toString());
  await fs.ensureDir(exportPath);
  await fs.emptyDir(exportPath);
  await GitClient.clone(getLocalRepoPath(user, project, version), exportPath, [
    '-c core.longpaths=true',
    '-c core.quotepath=off',
    '--depth=1',
    `--branch=${gitServer.getBranchName(version)}`]);
  await removeGitDirectory(exportPath);
  return exportPath;
}

async function searchFile({
  version, string,
  option,
}) {
  const url = version.repository.url.split('/');
  const versionId = url.pop();
  const projectId = url.pop();
  const localRepoPath = path.join(global.config.path.gitDir, projectId, versionId);
  // git grep --break --heading -n -i -w searchContent version(branch) --suffix`;
  const command = 'git';
  const args = ['grep', '--break', '--heading', '-n', '-E'];
  if (option && option.ignoreCase) {
    args.push('-i');
  }
  if (option && option.matchWholeWord) {
    args.push('-w');
  }
  args.push(string);
  args.push(version.currentCommitId || gitServer.getBranchName(version));
  args.push('--');
  option.suffixes.forEach((suffix) => {
    args.push(suffix);
  });
  const result = await new Promise((resolve, reject) => {
    const gitGrep = spawn(command, args, { cwd: path.resolve(process.cwd(), localRepoPath) });
    let result = '';
    let error = '';
    gitGrep.stdout.on('data', (data) => {
      result += data;
    });
    gitGrep.stderr.on('data', (data) => {
      error += data;
    });
    gitGrep.on('close', () => {
      if (error) reject(error);
      resolve(result);
    });
    gitGrep.on('error', (err) => {
      reject(err);
    });
  });
  return result;
}
/**
 * Export model definition object.
 */
module.exports = {
  archive,
  getLocalUserPath,
  getLocalRepoPath,
  getLocalBranchPath,

  getLatestCommitId,
  diffSummary,
  diffSummaryFormat,

  initRepo,
  emptyRepoDirectory,
  removeGitDirectory,

  checkoutBranch,
  createBranch,
  removeBranch,
  diffType,
  addAllFilesInDirectory,
  show,
  isNewEnviroment,
  searchFile,
};
exports = Object.assign(exports, module.exports);
